iT邦幫忙

2026 iThome 鐵人賽

DAY 18
0
Software Development

從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局系列 第 18

[Day 18] High-Performance Concurrency: Thread Pinning I

  • 分享至 

  • xImage
  •  

在一般應用程式中,thread 要在哪一顆 CPU 上執行,不會是開發者需要操心的問題。作業系統的 scheduler 會根據 CPU 負載、Thread priority、CPU topology、NUMA locality 以及其他排程因素,自動決定 runnable thread 應該被派到哪個 logical CPU。

這種設計對大多數 workload 都非常合理。但當系統進入另一種情境 —— 例如低延遲交易、封包處理、遊戲引擎、音訊處理、storage I/O、telemetry pipeline、訊息 broker 或高頻資料處理,各種隱憂就開始浮現。

這就是 thread pinning 的價值所在 —— 用 CPU affinity 降低 scheduler jitter,確保 critical thread 在需要執行的那一刻,能以高度確定性穩定取得 CPU。若再進一步結合 CPU isolation,更能排除其他程序與中斷的干擾,達到極致的低延遲。

1. 為什麼 Scheduler 會造成 Jitter?

現代作業系統的 scheduler 非常複雜。它的主要目標通常是讓整體系統具有良好的 throughput、fairness 與 responsiveness,而不是讓某一個 thread 永遠獲得最低可能的 scheduling latency。

while (running) {
    receive();
    process();
    send();
}

上述例子是一個 latency-sensitive loop。我們希望這個 loop 能夠持續以非常穩定的時間間隔執行,然而實際情況可能變成:
CPU
ㅤ├── critical thread
ㅤ├── kernel task
ㅤ├── interrupt handler
ㅤ├── another user thread
ㅤ├── kworker
ㅤ└── background process

當 critical thread 正在 CPU 上執行時,可能突然遇到:

  • Scheduler preemption
  • Timer interrupt
  • Hardware interrupt
  • Softirq
  • Kernel worker
  • Page fault
  • CPU migration
  • Cache contention
  • SMT sibling contention

因此,即使程式本身的 execution time 非常穩定,實際觀察到的 latency 仍可能出現尖峰。高效能 concurrency 的主要難題之一,是如何降低這些 tail latency。

2. Thread Pinning 到底在做什麼?

Thread pinning 的核心概念很簡單:限制某個 thread 可以在哪些 CPU 上執行。最常見的方式就是 CPU affinity。

例如一台機器有 8 個 CPU。一般情況下,一個 thread 可能可以被 scheduler 放到 CPU 0~7。如果我們把這個 critical thread pin 到 CPU 6,那麼 scheduler 就不會把這個 critical thread 任意 migrate 到其他 CPU。

在 Linux 上,可以透過 CPU affinity API 完成:

cpu_set_t cpuset;

CPU_ZERO(&cpuset);
CPU_SET(6, &cpuset);

pthread_setaffinity_np(
    pthread_self(),
    sizeof(cpu_set_t),
    &cpuset
);

這並不表示 CPU 6 比 CPU 0 快,而是降低了 scheduler 為了平衡整體系統而移動這個 thread 的自由度。這種控制對某些 workload 非常重要。

3. CPU Migration 為什麼值得注意?

Thread migration 看起來只是 CPU 2 → CPU 6 這麼輕易,但實際上會牽涉更多成本。最明顯的是 CPU cache locality。

假設 Thread 在 CPU 2 上執行一段時間:
CPU 2
ㅤ├── L1 cache
ㅤ├── L2 cache
ㅤ└── registers/state

它處理的資料可能已經大量存在於 CPU 2 的 cache hierarchy 中。如果 scheduler 將 Thread migrate 到 CPU 6,CPU 6 需要重新建立自己的 cache working set:

CPU 2ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤCPU 6
cacheㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ cache
ㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ │
ㅤ└──── thread ───────►

當然,這不代表每次 migration 都會造成巨大成本。現代 CPU 的 cache coherence 與 scheduler 都已經非常成熟,但是在極低 latency workload 中,即使微小的 locality 變化也可能反映在 tail latency。

因此 Thread pinning 的第二個目的就是維持 execution locality

4. Pinning 不等於「CPU 專用」

這是一個非常重要的觀念。很多人第一次接觸 affinity 時會認為:Thread A 綁定到 CPU 6 代表 CPU 6 專屬於 Thread A。其實並非如此,affinity 只表示限制該執行緒的調度範圍

Thread A
allowed CPUs = {6}

Thread A 只能在 CPU 6 上執行,但 CPU 6 上仍然可能存在於其他 thread,或是 IRQ、softirq、kernel worker 和 other kernel activity。所以 pinning 與 CPU isolation 是兩個不同層次的概念。前者控制這個 thread 可以去哪個 CPU;後者則更進一步控制這顆 CPU 上還允許什麼工作發生。這也是為什麼真正的 high-performance setup 往往不只使用 thread pinning。

5. 從 CPU Affinity 走向 Isolated CPU

如果我們真的希望建立一個 latency-sensitive execution core,可以考慮把 CPU topology 設計成:
CPU 0ㅤsystem / housekeeping
CPU 1ㅤsystem / housekeeping
CPU 2ㅤsystem / housekeeping
CPU 3ㅤsystem / housekeeping

CPU 4ㅤisolated
CPU 5ㅤisolated
CPU 6ㅤisolated
CPU 7ㅤisolated

Application
ㅤ├── networking thread → CPU 4
ㅤ├── processing thread → CPU 6
ㅤ└── worker thread → CPU 7

系統的一般背景工作盡量集中在 housekeeping CPUs。這樣做並非是為了讓 CPU 4~7 完全沒有任何作業系統活動,實際上現代 OS 也很難做到絕對意義上的零干擾。真正的目標是把不可避免的 system activity 與 critical execution path 分離,因此可以把整體架構想成:

ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ┌───────────┐
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ│ㅤHousekeeping CPUㅤㅤ│
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ│ㅤㅤOS / IRQ / miscㅤㅤㅤ│
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ└─────┬─────┘
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ⠀ㅤ│
ㅤ⠀Applicationㅤ────────┼───────
ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ⠀ㅤ│
ㅤㅤㅤㅤㅤㅤㅤㅤㅤ┌──────┴──────┐
ㅤㅤㅤㅤㅤㅤCPU 4 isolatedㅤㅤㅤㅤㅤㅤㅤㅤㅤCPU 6 isolated
ㅤㅤㅤㅤㅤㅤㅤㅤㅤ│ㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤ│
ㅤㅤㅤㅤㅤㅤㅤRX loopㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤㅤprocessing loop
ㅤㅤㅤㅤㅤㅤㅤㅤㅤ└────ㅤdataㅤ─────┘


上一篇
[Day 17] High-Performance Concurrency: Lock-Free Programming II
下一篇
[Day 19] High-Performance Concurrency: Thread Pinning II
系列文
從 C++ 菜鳥到 Low-Latency 勇者:一場分秒必爭的賽局21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言